﻿'HIDEasy
'Copyright (C) 2011 ASFX

'This program is free software; you can redistribute it and/or
'modify it under the terms of the GNU General Public License
'as published by the Free Software Foundation; either version 2
'of the License, or (at your option) any later version.

'This program is distributed in the hope that it will be useful,
'but WITHOUT ANY WARRANTY; without even the implied warranty of
'MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
'GNU General Public License for more details.

'You should have received a copy of the GNU General Public License
'along with this program; if not, write to the Free Software
'Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

Imports System.ComponentModel
Imports USAGE = System.UInt16

<CLSCompliant(False)> _
Public Class HIDItem
    Implements IDisposable

#Region "Variables"
    Private _DataIndex As UShort
    Private _ValueExists As Boolean
    Private _Value As Int32
    Private _IsButton As Boolean

    Private _FriendlyName As String
    Private _Usage As USAGE
    Private _UsagePage As USAGE
    Private _ReportID As Byte
    ' UCHAR
    Private _BitField As UShort
    Private _LinkCollection As UShort
    Private _LinkUsage As USAGE
    Private _LinkUsagePage As USAGE
    Private _StringIndex As UShort
    Private _DesignatorIndex As UShort

    Private _IsAbsolute As Boolean
    Private _HasNull As Boolean
    'Private _Reserved As Byte 'UCHAR
    Private _BitSize As UShort

    Private _UnitsExp As UInt32
    Private _Units As Int32
    Private _LogicalMin As Int32
    Private _LogicalMax As Int32
    Private _PhysicalMin As Int32
    Private _PhysicalMax As Int32

    Private _RadioButtonGroup As UInt32 = 0

    Private _HasChanged As Boolean

    Private _ExtendedAttributes As List(Of Unknown_Token)

    Private _FirstItemArray As HIDItem = Nothing
    Private _NextItemArray As HIDItem = Nothing
    Private _Parent As HIDItemCollection
    Private _String As String

    Private _LockObject As New Object
#End Region

#Region "Events"
    Public Event Changed As EventHandler(Of HIDItemChangedEventArgs)
#End Region

#Region "Enums and Structures"

    Structure Unknown_Token
        Dim Token As Byte
        'UCHAR 
        '<MarshalAs(UnmanagedType.ByValArray, SizeConst:=3)> _
        'Private Reserved() As Char 'UCHAR
        Dim BitField As UInt32
        'ULong
        Public Overrides Function ToString() As String
            Dim _str As String = ""
            _str &= "Token: 0x" & Token.ToString("X2")
            _str &= "Bit Field: 0x" & Token.ToString("X8")
            Return _str
        End Function
    End Structure

#End Region

#Region "Creator"

    Friend Sub New(ByVal IsRange As Boolean, _
                   ByVal IsAlias As Boolean, _
                   ByVal IsStringRange As Boolean, _
                   ByVal IsDesignatorRange As Boolean, _
                   ByVal Range As HIDClass.HIDP_BUTTON_Or_VALUE_CAPS_Range, _
                   ByVal NotRange As HIDClass.HIDP_BUTTON_Or_VALUE_CAPS_NotRange, _
                   Optional ByVal IndexInRange As UShort = 0)

        _ExtendedAttributes = New List(Of Unknown_Token)

        If IsRange Then
            With Range
                If (.DataIndexMin + IndexInRange) > .DataIndexMax Then
                    Throw New HIDException("Unable to create HIDItem (DataIndexMin + IndexInRange>DataIndexMax).")
                Else
                    _DataIndex = .DataIndexMin + IndexInRange
                End If

                If Not IsAlias Then
                    If (.UsageMin + IndexInRange) > .UsageMax Then
                        Throw New HIDException("Unable to create HIDItem (UsageMin + IndexInRange>UsageMax).")
                    Else
                        _Usage = .UsageMin + IndexInRange
                    End If
                Else
                    _Usage = .UsageMin
                End If

                If IsStringRange Then
                    If (.StringMin + IndexInRange) > .StringMax Then
                        Throw New HIDException("Unable to create HIDItem (StringMin + IndexInRange>StringMax).")
                    Else
                        _StringIndex = .StringMin + IndexInRange
                    End If
                Else
                    _StringIndex = .StringMin
                End If

                If IsDesignatorRange Then
                    If (.DesignatorMin + IndexInRange) > .DesignatorMax Then
                        Throw New HIDException("Unable to create HIDItem (DesignatorMin + IndexInRange>DesignatorMax).")
                    Else
                        _DesignatorIndex = .DesignatorMin + IndexInRange
                    End If
                Else
                    _DesignatorIndex = .DesignatorMin
                End If

            End With

        Else
            With NotRange
                _Usage = .Usage
                _StringIndex = .StringIndex
                _DesignatorIndex = .DesignatorIndex

                If IndexInRange <> 0 Then
                    Throw New HIDException("Unable to create HIDItem (IsRange=False and IndexInRange<>0).")
                End If
                _DataIndex = .DataIndex
            End With
        End If
    End Sub

    Friend Sub New(ByVal ButtonCaps As HIDClass.HIDP_BUTTON_CAPS, _
                   Optional ByVal IndexInRange As UShort = 0)
        Me.New(ButtonCaps.IsRange, ButtonCaps.IsAlias, ButtonCaps.IsStringRange, ButtonCaps.IsDesignatorRange, _
               ButtonCaps.Range, ButtonCaps.NotRange, _
               IndexInRange)

        _IsButton = True
        _ValueExists = False

        With ButtonCaps
            _UsagePage = .UsagePage
            _ReportID = .ReportID
            '_IsAlias 
            _BitField = .BitField
            _LinkCollection = .LinkCollection
            _LinkUsage = .LinkUsage
            _LinkUsagePage = .LinkUsagePage
            _IsAbsolute = .IsAbsolute
        End With

        _LogicalMin = 0
        _LogicalMax = 1
        _PhysicalMin = 0
        _PhysicalMax = 1

        _Value = (_LogicalMin + _LogicalMax) >> 1

        SetDefaultFriendlyName()
    End Sub

    Friend Sub New(ByVal ValueCaps As HIDClass.HIDP_VALUE_CAPS, _
                   Optional ByVal IndexInRange As UShort = 0)
        Me.New(ValueCaps.IsRange, ValueCaps.IsAlias, ValueCaps.IsStringRange, ValueCaps.IsDesignatorRange, _
               ValueCaps.Range, ValueCaps.NotRange, _
               IndexInRange)

        _IsButton = False
        _ValueExists = False

        With ValueCaps
            _UsagePage = .UsagePage
            _ReportID = .ReportID
            ' .IsAlias Not Used
            _BitField = .BitField
            _LinkCollection = .LinkCollection
            _LinkUsage = .LinkUsage
            _LinkUsagePage = .LinkUsagePage

            _HasNull = .HasNull
            _IsAbsolute = .IsAbsolute
            _BitSize = .BitSize

            _UnitsExp = .UnitsExp
            _Units = .Units
            _LogicalMin = .LogicalMin
            _LogicalMax = .LogicalMax
            If _LogicalMax < _LogicalMin Then
                _LogicalMin = .LogicalMin
                _LogicalMax = .LogicalMin + CInt((2 ^ _BitSize) - 1)
            End If
            If .PhysicalMin = 0 And .PhysicalMax = 0 Then
                _PhysicalMin = _LogicalMin
                _PhysicalMax = _LogicalMax
            Else
                _PhysicalMin = .PhysicalMin
                _PhysicalMax = .PhysicalMax
                If _PhysicalMax < _PhysicalMin Then
                    _PhysicalMin = .PhysicalMin
                    _PhysicalMax = .PhysicalMin + CInt((2 ^ _BitSize) - 1)
                End If
            End If
        End With
        _Value = (_LogicalMin + _LogicalMax) >> 1

        SetDefaultFriendlyName()
    End Sub

#End Region

#Region "Properties"
    Public ReadOnly Property FriendlyName As String
        Get
            Return _FriendlyName
        End Get
    End Property
    <Category("Usages and Report")> _
    Public ReadOnly Property Usage As USAGE
        Get
            Return _Usage
        End Get
    End Property
    <Category("Usages and Report")> _
    Public ReadOnly Property UsagePage As USAGE
        Get
            Return _UsagePage
        End Get
    End Property
    <Category("Usages and Report")> _
    Public ReadOnly Property ReportID As Byte
        Get
            Return _ReportID
        End Get
    End Property
    <Category("Usages and Report")> _
    Public ReadOnly Property DataIndex As UShort
        Get
            Return _DataIndex
        End Get
    End Property

    <Category("Values")> _
Public Property LogicalValue As Int32
        Get
            SyncLock Me.SyncRoot
                If _ValueExists Then
                    Return _Value
                Else
                    Throw New NotSupportedException("Logical Value doesn't exists in this HID Item.")
                End If
            End SyncLock
        End Get
        Set(ByVal value As Int32)
            SyncLock Me.SyncRoot
                If _Parent Is Nothing Then
                    SetLogicalValue(value)
                Else
                    Select Case _Parent.ReportType
                        Case HIDReport.ReportType.Input
                            Throw New HIDException("Unable to set a LogicalValue. LogicalValue is readonly.")
                        Case HIDReport.ReportType.Output, HIDReport.ReportType.Feature
                            SetLogicalValue(value)
                    End Select
                End If
            End SyncLock
        End Set
    End Property
    '<Category("Values")> _
    'Public ReadOnly Property PhysicalValue As Int32
    '    Get
    '       SyncLock Me.SyncRoot
    '        If _ValueExists Then
    '            Return CInt((_Value - _LogicalMin) _
    '                * ((_PhysicalMax - _PhysicalMin) _
    '                    / (_LogicalMax - _LogicalMin)) _
    '                + _PhysicalMin)
    '        Else
    '            Throw New System.HIDException("Physical Value doesn't exists in this HID Item.")
    '        End If
    '       end syncklock
    '    End Get
    'End Property

    <Category("Values")> _
Public ReadOnly Property ValueExists As Boolean
        Get
            Return _ValueExists
        End Get
    End Property

    <Category("Misc")> _
    Public ReadOnly Property IsButton As Boolean
        Get
            Return _IsButton
        End Get
    End Property

    <Category("Values")> _
    Public ReadOnly Property ButtonIsOn As Boolean
        Get
            SyncLock Me.SyncRoot
                If _IsButton Then
                    Return _Value <> 0
                Else
                    Throw New NotSupportedException("This HID Item is not a button.")
                End If
            End SyncLock
        End Get
    End Property

    <Category("Misc")> _
    Public ReadOnly Property IsAbsolute As Boolean
        Get
            Return _IsAbsolute
        End Get
    End Property

    <Category("Values")> _
    Public ReadOnly Property HasNull As Boolean
        Get
            Return _HasNull
        End Get
    End Property

    <Category("Values")> _
    Public ReadOnly Property LogicalMin As Int32
        Get
            Return _LogicalMin
        End Get
    End Property

    <Category("Values")> _
    Public ReadOnly Property LogicalMax As Int32
        Get
            Return _LogicalMax
        End Get
    End Property

    <Category("Values")> _
    Public ReadOnly Property PhysicalMin As Int32
        Get
            Return _PhysicalMin
        End Get
    End Property

    <Category("Values")> _
    Public ReadOnly Property PhysicalMax As Int32
        Get
            Return _PhysicalMax
        End Get
    End Property

    <Category("LinkCollection")> _
    Public ReadOnly Property LinkCollection As UShort
        Get
            Return _LinkCollection
        End Get
    End Property

    <Category("LinkCollection")> _
    Public ReadOnly Property LinkUsage As UShort
        Get
            Return _LinkUsage
        End Get
    End Property

    <Category("LinkCollection")> _
    Public ReadOnly Property LinkUsagePage As UShort
        Get
            Return _LinkUsagePage
        End Get
    End Property

    <Category("Misc")> _
    Public ReadOnly Property BitSize As UShort
        Get
            Return _BitSize
        End Get
    End Property

    <Category("Misc")> _
    Public ReadOnly Property BitField As UShort
        Get
            Return _BitField
        End Get
    End Property

    <Category("Misc")> _
    Public ReadOnly Property StringIndex As UShort
        Get
            Return _StringIndex
        End Get
    End Property

    <Category("Radio Button")> _
    Public ReadOnly Property IsRadioButton As Boolean
        Get
            Return _RadioButtonGroup <> 0
        End Get
    End Property

    <Category("Radio Button")> _
    Public Property RadioButtonGroup As UInt32
        Get
            Return _RadioButtonGroup
        End Get
        Friend Set(ByVal value As UInt32)
            _RadioButtonGroup = value
        End Set
    End Property

    <Category("Values")> _
    Public Property HasChanged As Boolean
        Get
            Return _HasChanged
        End Get
        Friend Set(ByVal value As Boolean)
            _HasChanged = value
        End Set
    End Property

    <Category("Array")> _
    Public ReadOnly Property IsInAnArray As Boolean
        Get
            Return (_FirstItemArray IsNot Nothing)
        End Get
    End Property

    <Category("Array")> _
    Public Property FirstArrayItem As HIDItem
        Get
            Return _FirstItemArray
        End Get
        Friend Set(ByVal value As HIDItem)
            _FirstItemArray = value
        End Set
    End Property

    <Category("Array")> _
    Public Property NextArrayItem As HIDItem
        Get
            Return _NextItemArray
        End Get
        Friend Set(ByVal value As HIDItem)
            _NextItemArray = value
        End Set
    End Property

    <Browsable(False)>
    Public Property Parent As HIDItemCollection
        Get
            Return _Parent
        End Get
        Friend Set(ByVal value As HIDItemCollection)
            _Parent = value
        End Set
    End Property

    <Browsable(False)> _
    Public ReadOnly Property SyncRoot() As Object
        Get
            If _Parent Is Nothing Then
                Return _LockObject
            Else
                Return _Parent.SyncRoot
            End If
        End Get
    End Property

    <Category("Misc")> _
    Public Property [String] As String
        Get
            Return _String
        End Get
        Friend Set(ByVal value As String)
            _String = value
        End Set
    End Property

    <Category("Misc")> _
    Public Property ExtendedAttributes As List(Of Unknown_Token)
        Get
            Return _ExtendedAttributes
        End Get
        Friend Set(ByVal value As List(Of Unknown_Token))
            _ExtendedAttributes = value
        End Set
    End Property

#End Region

#Region "Set XXXX"
    Friend Sub SetFriendlyName(ByVal newFriendlyName As String)
        _FriendlyName = newFriendlyName
    End Sub
    Friend Sub SetDefaultFriendlyName()
        _FriendlyName = GetUsagePageString(_LinkUsagePage).Replace("/"c, "_").Trim & ", " _
            & GetUsageIDString(_LinkUsagePage, _LinkUsage).Replace("/"c, "_").Trim & " / " _
            & GetUsagePageString(_UsagePage).Replace("/"c, "_").Trim & ", " _
            & GetUsageIDString(_UsagePage, _Usage).Replace("/"c, "_").Trim
    End Sub
#Region "Set Logical Value"

    Friend Sub SetLogicalRawValue(ByVal RawValue As UInt32, _
                                  Optional ByVal NoEvent As Boolean = False)
        Dim _OldValue As Integer = _Value
        Dim _OldValueExists As Boolean = _ValueExists
        Dim _CurrentValue As Int64
        SyncLock Me.SyncRoot
            If _IsAbsolute Then
                If _LogicalMin < 0 Then
                    Dim _CompMask As UInt32 = CUInt(2 ^ CUInt(_BitSize - 1))
                    Dim _DataMask As UInt32 = CUInt(_CompMask - 1)
                    If (_CompMask And RawValue) <> 0 Then
                        ' Negative Number
                        _CurrentValue = CLng(RawValue And _DataMask) - CLng(_CompMask)
                    Else
                        'Positive Number
                        _CurrentValue = RawValue
                    End If
                Else
                    _CurrentValue = RawValue
                End If
            End If

            SetLogicalValue(CType(_CurrentValue, Integer), NoEvent)
        End SyncLock
    End Sub
    Private Sub SetLogicalValue(ByVal NewValue As Int32, _
                                  Optional ByVal NoEvent As Boolean = False)
        SyncLock Me.SyncRoot
            Dim _OldValue As Integer = _Value
            Dim _OldValueExists As Boolean = _ValueExists
            Dim _CurrentValue As Int64 = NewValue

            If _IsAbsolute Then
                'Absolute Value
                If _CurrentValue >= _LogicalMin And _CurrentValue <= _LogicalMax Then
                    If _Value <> _CurrentValue Then
                        _Value = CInt(_CurrentValue)
                    End If
                    _ValueExists = True
                Else
                    If _HasNull Then
                        _ValueExists = False
                    Else
                        _ValueExists = False
                    End If
                End If
            Else
                'Relative Value (Change from previous state)
                _ValueExists = True
                Select Case _CurrentValue
                    Case Is = 0
                        ' No Change!!!
                    Case Is > 0
                        Try
                            If (_CurrentValue + _Value) > _LogicalMax Then
                                _Value = _LogicalMax
                                If _HasNull Then
                                    _ValueExists = False
                                Else
                                    _ValueExists = True
                                End If
                            Else
                                _Value = _Value + CInt(_CurrentValue)
                                _ValueExists = True
                            End If
                        Catch ex As Exception
                            If _HasNull Then
                                _ValueExists = False
                            Else
                                _ValueExists = True
                            End If
                            _Value = _LogicalMax
                        End Try
                    Case Is < 0
                        Try
                            If (_CurrentValue + _Value) < _LogicalMin Then
                                _Value = _LogicalMin
                                If _HasNull Then
                                    _ValueExists = False
                                Else
                                    _ValueExists = True
                                End If
                            Else
                                _Value = _Value + CInt(_CurrentValue)
                                _ValueExists = True
                            End If
                        Catch ex As Exception
                            If _HasNull Then
                                _ValueExists = False
                            Else
                                _ValueExists = True
                            End If
                            _Value = _LogicalMin
                        End Try
                End Select
            End If
            If _OldValue <> _Value Or _OldValueExists <> _ValueExists Then
                _HasChanged = True
            Else
                Exit Sub
            End If
        End SyncLock
        If _HasChanged AndAlso Not NoEvent Then RaiseEvent Changed(Me, New HIDItemChangedEventArgs(Me))
    End Sub
    Friend Sub SetLogicalValueMax(Optional ByVal NoEvent As Boolean = False)
        SyncLock Me.SyncRoot
            If _Value <> _LogicalMax Or Not _ValueExists Then
                _ValueExists = True
                _Value = _LogicalMax
                _HasChanged = True
            Else
                Exit Sub
            End If
        End SyncLock
        If _HasChanged AndAlso Not NoEvent Then RaiseEvent Changed(Me, New HIDItemChangedEventArgs(Me))
    End Sub
    Friend Sub SetLogicalValueMin(Optional ByVal NoEvent As Boolean = False)
        SyncLock Me.SyncRoot
            If _Value <> _LogicalMin Or Not _ValueExists Then
                _ValueExists = True
                _Value = _LogicalMin
                _HasChanged = True
            Else
                Exit Sub
            End If
        End SyncLock
        If _HasChanged AndAlso Not NoEvent Then RaiseEvent Changed(Me, New HIDItemChangedEventArgs(Me))
    End Sub

#End Region
#End Region

#Region "Misc"

    Friend Function GetHIDP_DATA() As HIDClass.HIDP_DATA
        SyncLock Me.SyncRoot
            Dim _Hidp_Data As HIDClass.HIDP_DATA
            With _Hidp_Data
                .DataIndex = _DataIndex
                If Me.IsButton Then
                    .Values.On = Me.ButtonIsOn
                Else
                    Try
                        .Values.RawValue = CUInt(_Value)
                    Catch ex As OverflowException
                        ' Negative number
                        Dim _ULogicalValue As UInteger
                        _ULogicalValue = 1UI << (_BitSize - 1)
                        _ULogicalValue += CUInt(Math.Abs(_Value))
                        .Values.RawValue = _ULogicalValue
                    End Try
                End If
            End With
            Return _Hidp_Data
        End SyncLock
    End Function

#End Region

#Region "IDisposable Support"
    Private disposedValue As Boolean
    ' Pour détecter les appels redondants

    ' IDisposable
    Protected Overridable Sub Dispose(ByVal disposing As Boolean)
        If Not Me.disposedValue Then
            If disposing Then
                _ExtendedAttributes.Clear()
                _ExtendedAttributes = Nothing
                _FirstItemArray = Nothing
                _NextItemArray = Nothing
                _Parent = Nothing
            End If
        End If
        Me.disposedValue = True
    End Sub

    Protected Overrides Sub Finalize()
        Dispose(False)
        MyBase.Finalize()
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub

#End Region

#Region "Overrides base Class (Object)"

    Public Overrides Function ToString() As String
        SyncLock Me.SyncRoot
            Dim str As String = ""
            str &= "FriendlyName: " & _FriendlyName.ToString & vbCrLf
            str &= "DataIndex: " & _DataIndex.ToString & vbCrLf
            str &= "ReportID: " & _ReportID.ToString & " "
            str &= "LinkCollection: " & _LinkCollection.ToString() & vbCrLf
            str &= "LinkUsagePage: " & _LinkUsagePage.ToString() & " - " & GetUsagePageString(_LinkUsagePage) & vbCrLf
            str &= "LinkUsage: " & _LinkUsage.ToString() & " - " & GetUsageIDString(_LinkUsagePage, _LinkUsage) & vbCrLf
            str &= "UsagePage: " & _UsagePage.ToString() & " - " & GetUsagePageString(_UsagePage) & vbCrLf
            str &= "Usage: " & _Usage.ToString() & " - " & GetUsageIDString(_UsagePage, _Usage) & vbCrLf
            str &= "StringIndex: " & _StringIndex.ToString & " - String: " _
                   & IIf(_String Is Nothing, "[Nothing]", _String).ToString & vbCrLf
            str &= "DesignatorIndex: " & _DesignatorIndex.ToString & vbCrLf
            str &= "Value: " & IIf(_ValueExists, _Value.ToString(), "<No Value>").ToString & " "
            str &= "(Logical Min: " & _LogicalMin.ToString() & " Max: " & _LogicalMax.ToString() & ", Physical Min: " _
                   & _PhysicalMin.ToString() & " Max: " & _PhysicalMax.ToString() & ")" & vbCrLf
            If _IsButton Then str &= "Button:" & IIf(Me.ButtonIsOn, "On", "Off").ToString & vbCrLf
            str &= "HasChanged: " & IIf(_HasChanged, "Yes", "No").ToString & vbCrLf
            str &= "IsAbsolute: " & IIf(_IsAbsolute, "Yes", "No").ToString & vbCrLf
            str &= "HasNull: " & IIf(_HasNull, "Yes", "No").ToString & vbCrLf
            str &= "BitSize: " & _BitSize.ToString & vbCrLf
            str &= "UnitsExp: " & _UnitsExp.ToString & vbCrLf
            str &= "Units: " & _Units.ToString & vbCrLf
            If _RadioButtonGroup <> 0 Then str &= "(Radio Btn Group: " & _RadioButtonGroup.ToString & ")" & vbCrLf
            str &= "Array: (First Item: "
            If _FirstItemArray Is Nothing Then
                str &= "-"
            Else
                str &= _FirstItemArray.DataIndex.ToString()
            End If
            str &= " Next: "
            If _NextItemArray Is Nothing Then
                str &= "-)" & vbCrLf
            Else
                str &= _NextItemArray.DataIndex.ToString & ")" & vbCrLf
            End If
            str &= "Extended Attributes: (Items: " & _ExtendedAttributes.Count.ToString & ")" & vbCrLf
            For Each _ut As Unknown_Token In _ExtendedAttributes
                str &= _ut.ToString() & vbCrLf
            Next
            Return str
        End SyncLock
    End Function

#End Region
End Class
